Skip to content

feat: add shared database-backed policy bundle - #14426

Merged
erichare merged 4 commits into
release-1.12.0from
fix/shared-policy-bundle
Aug 5, 2026
Merged

feat: add shared database-backed policy bundle#14426
erichare merged 4 commits into
release-1.12.0from
fix/shared-policy-bundle

Conversation

@erichare

@erichare erichare commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Add one immutable, revisioned database policy bundle for the model-provider allowlist, component blocklist, and starter-template blocklist.
  • Add superuser read, CAS replacement, history, and rollback APIs, plus a shared runtime coordinator and cross-replica refresh path.
  • Add an additive migration that seeds from the legacy provider/catalog stores, dual-writes compatibility views, and provides an exact-head guarded downgrade seam.
  • Reject stale writes and catalog plugins that cannot apply the shared bundle instead of silently leaving enforcement out of sync.

Deployment notes

  • A committed database revision converges across current-version replicas through the refresh worker; a cluster restart is not required.
  • During a mixed old/new rolling deployment, freeze every provider/catalog policy write at ingress. Compatibility is one-way: new writers update legacy tables, but old writers do not update the new bundle.
  • The previously shipped N-1 image cannot resolve the new base migration head and therefore requires a pre-upgrade database restore. The release-owned in-place downgrade copies the active bundle back to legacy stores only for a separately qualified rollback image based on the new OSS base.

Validation

  • Backend policy/API/migration suite: 106 passed, 6 skipped
  • Isolated LFX provider/catalog suite: 48 passed
  • Enterprise compatibility contract and service-resolution checks: passed against this commit
  • Ruff format/check, git diff --check, Alembic expand-contract validation, and secret detection: passed
  • Independent Python, security, and final code reviews: no unresolved findings

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This change adds a shared immutable policy-bundle service, durable revision storage, migration support, superuser administration endpoints, runtime synchronization, legacy compatibility paths, and comprehensive tests.

Changes

Shared policy bundle

Layer / File(s) Summary
Runtime policy-bundle contract
src/lfx/src/lfx/services/policy_bundle/*, src/lfx/src/lfx/services/catalog_policy/*, src/lfx/src/lfx/services/model_provider_policy/*
Defines immutable snapshots, canonical hashes, publication rules, source availability, and catalog/provider integration.
Durable revisions and migration lifecycle
src/backend/base/langflow/services/policy_bundle.py, src/backend/base/langflow/services/database/models/policy_bundle/*, src/backend/base/langflow/alembic/versions/*, src/backend/base/langflow/services/database/service.py
Adds revision persistence, compare-and-set updates, rollback, bootstrap, legacy synchronization, migration upgrade/downgrade, and guarded downgrade execution.
Backend service integration
src/backend/base/langflow/services/catalog_policy/*, src/backend/base/langflow/services/model_provider_policy.py, src/backend/base/langflow/services/task/*, src/backend/base/langflow/services/utils.py
Hydrates and publishes shared policy state while preserving legacy paths and updating refresh failure handling.
Policy-bundle administration API
src/backend/base/langflow/api/v1/policy_bundle.py, src/backend/base/langflow/api/v1/catalog_policy.py, src/backend/base/langflow/api/v1/model_provider_policy.py, src/backend/base/langflow/api/router.py
Adds superuser-only read, replace, history, and rollback routes with validation, revision conflicts, auditing, and runtime publication.
Validation coverage
src/backend/tests/unit/alembic/*, src/backend/tests/unit/api/v1/*, src/backend/tests/unit/services/*, src/lfx/tests/unit/services/*
Tests migration recovery, API authorization and conflicts, persistence CAS and rollback, runtime publication, refresh behavior, and compatibility handling.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Quality And Coverage ⚠️ Warning Async pytest patterns are correct, and core CAS/API flows are tested, but migration validation/downgrade synchronization, policy-bundle API error branches, and LFX coordinator lifecycle/conflict be... Add tests for missing/partial migration schemas, non-initialized/conflicting/external API cases, active-bundle downgrade synchronization, async downgrade delegation, and PolicyBundleService publish, recovery, and teardown branches.
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed The commit adds 9 named test_*.py files with substantive tests for migration, CAS/rollback/bootstrap, API authorization/history, refresh, compatibility errors, and cross-worker runtime publication.
Test File Naming And Structure ✅ Passed All 9 changed test files use test_*.py under unit paths, pytest discovers async tests automatically, fixtures clean up resources, and cases cover successful, conflict, validation, auth, migration,...
Excessive Mock Usage Warning ✅ Passed Mocks isolate API, logging, Alembic, and dependency lookups; core bundle behavior uses real SQLite sessions and concrete services in migration, store, and runtime tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a shared database-backed policy bundle.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shared-policy-bundle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Migration Validation Passed

All migrations follow the Expand-Contract pattern correctly.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.39306% with 108 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.23%. Comparing base (83b26b2) to head (8bcac79).
⚠️ Report is 2 commits behind head on release-1.12.0.

Files with missing lines Patch % Lines
...rc/backend/base/langflow/services/policy_bundle.py 83.72% 28 Missing ⚠️
src/lfx/src/lfx/services/policy_bundle/service.py 67.79% 17 Missing and 2 partials ⚠️
.../src/lfx/services/model_provider_policy/service.py 51.61% 12 Missing and 3 partials ⚠️
...nd/base/langflow/services/model_provider_policy.py 75.00% 8 Missing ⚠️
src/backend/base/langflow/api/v1/policy_bundle.py 93.85% 7 Missing ⚠️
src/lfx/src/lfx/services/manager.py 46.15% 6 Missing and 1 partial ⚠️
...d/base/langflow/services/catalog_policy/service.py 92.30% 6 Missing ⚠️
src/lfx/src/lfx/services/deps.py 14.28% 6 Missing ⚠️
src/lfx/src/lfx/services/catalog_policy/base.py 54.54% 5 Missing ⚠️
src/lfx/src/lfx/services/catalog_policy/service.py 76.92% 3 Missing ⚠️
... and 2 more
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.12.0   #14426      +/-   ##
==================================================
+ Coverage           61.95%   63.23%   +1.28%     
==================================================
  Files                2417     2390      -27     
  Lines              242524   243207     +683     
  Branches            36184    35173    -1011     
==================================================
+ Hits               150251   153789    +3538     
+ Misses              90354    87493    -2861     
- Partials             1919     1925       +6     
Flag Coverage Δ
backend 70.63% <90.19%> (+0.52%) ⬆️
frontend 61.69% <ø> (+1.83%) ⬆️
lfx 61.09% <68.13%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/backend/base/langflow/api/router.py 100.00% <100.00%> (ø)
src/backend/base/langflow/api/v1/catalog_policy.py 100.00% <100.00%> (ø)
...kend/base/langflow/api/v1/model_provider_policy.py 98.66% <100.00%> (+0.11%) ⬆️
...ckend/base/langflow/api/v1/policy_bundle_errors.py 100.00% <100.00%> (ø)
...end/base/langflow/api/v1/schemas/catalog_policy.py 100.00% <100.00%> (ø)
...d/base/langflow/services/catalog_policy/factory.py 100.00% <100.00%> (ø)
...ow/services/database/models/policy_bundle/model.py 100.00% <100.00%> (ø)
src/backend/base/langflow/services/deps.py 93.57% <100.00%> (-0.71%) ⬇️
src/backend/base/langflow/services/factory.py 85.07% <100.00%> (+0.45%) ⬆️
src/backend/base/langflow/services/schema.py 100.00% <100.00%> (ø)
... and 15 more

... and 565 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erichare
erichare force-pushed the fix/shared-policy-bundle branch from 25f0ff6 to 89d0cf5 Compare August 5, 2026 19:14
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/tests/unit/api/v1/test_policy_bundle.py (1)

295-324: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add rollback conflict coverage.

Configure rollback_policy_bundle_state to raise PolicyBundleRevisionConflictError. Assert HTTP 409 and confirm that apply_policy_bundle_state is not called. This endpoint currently tests only its success response.

As per coding guidelines, “API endpoint tests in the backend should verify both success and error responses.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/api/v1/test_policy_bundle.py` around lines 295 - 324,
Add a conflict-path test alongside
test_rollback_endpoint_creates_and_publishes_a_new_revision by configuring
rollback_state (the rollback_policy_bundle_state mock) to raise
PolicyBundleRevisionConflictError. Post the rollback request and assert HTTP
409, then verify apply_state (apply_policy_bundle_state) was not called.

Source: Coding guidelines

🧹 Nitpick comments (10)
src/backend/tests/unit/alembic/test_shared_policy_bundle_migration.py (1)

222-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add negative tests for the migration's fail-closed guards.

The three tests cover the success and repair paths. The two RuntimeError guards in the migration have no coverage:

  • upgrade raises "Shared policy bundle schema is partially initialized with durable data" when exactly one table exists and it holds rows.
  • _seed_active_bundle raises "Active policy bundle points to a missing immutable revision" when the active pointer references an absent revision.

These guards prevent a corrupt bundle state from being treated as valid. Add a test for each so a regression cannot turn a loud failure into a silent one. The existing _create_legacy_policy_tables and migration._create_revision_table seams make both cases cheap to set up.

As per coding guidelines: "Backend test files should ... be organized with descriptive test function names, logical setup/teardown, and coverage for positive, negative, edge, and error cases."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/alembic/test_shared_policy_bundle_migration.py` around
lines 222 - 244, Add two negative migration tests covering the fail-closed
guards: one should create exactly one bundle table with durable rows and assert
upgrade raises the expected partial-initialization RuntimeError; the other
should create the active bundle pointer referencing a nonexistent immutable
revision and assert _seed_active_bundle raises its missing-revision
RuntimeError. Use _create_legacy_policy_tables and
migration._create_revision_table for setup, and give each test a descriptive
name.

Source: Coding guidelines

src/backend/base/langflow/alembic/versions/f7a9c2d4e6b8_add_shared_policy_bundle.py (1)

325-332: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider dropping a single leftover table during downgrade.

When only one of the two tables exists, downgrade returns without dropping it. Alembic still records the revision as reverted, so an empty policy_bundle_revision or policy_bundle_active table stays in the schema. The upgrade path repairs that state, so the impact is limited to a leftover empty table. Dropping whichever table exists makes the downgrade fully reversible.

♻️ Proposed change
 def downgrade() -> None:
     """Copy the active bundle to legacy stores before removing new tables."""
     conn = op.get_bind()
-    if not migration.table_exists(REVISION_TABLE, conn) or not migration.table_exists(ACTIVE_TABLE, conn):
-        return
-    _sync_legacy_policy(conn)
-    op.drop_table(ACTIVE_TABLE)
-    op.drop_table(REVISION_TABLE)
+    revision_exists = migration.table_exists(REVISION_TABLE, conn)
+    active_exists = migration.table_exists(ACTIVE_TABLE, conn)
+    if revision_exists and active_exists:
+        _sync_legacy_policy(conn)
+    if active_exists:
+        op.drop_table(ACTIVE_TABLE)
+    if revision_exists:
+        op.drop_table(REVISION_TABLE)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/backend/base/langflow/alembic/versions/f7a9c2d4e6b8_add_shared_policy_bundle.py`
around lines 325 - 332, Update downgrade() to handle partial table state:
synchronize legacy policy data only when both REVISION_TABLE and ACTIVE_TABLE
exist, then drop each of those tables independently when it exists. Remove the
early return that currently preserves a lone table, while retaining safe
existence checks before every drop.
src/lfx/src/lfx/services/manager.py (1)

536-541: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the load-failure path as strict as the type check.

Line 539 raises RuntimeError when the configured class has the wrong type. However, Line 512 returns silently when load_object_from_import_path returns None for POLICY_BUNDLE_SERVICE. An operator who configures a policy bundle service that fails to import then starts with the built-in allow-all bundle service and no error. Align the two paths, as done for MODEL_PROVIDER_POLICY_SERVICE.

♻️ Proposed change at Lines 512-519
         if service_class is None:
-            if service_type == ServiceType.MODEL_PROVIDER_POLICY_SERVICE:
+            if service_type in {
+                ServiceType.MODEL_PROVIDER_POLICY_SERVICE,
+                ServiceType.POLICY_BUNDLE_SERVICE,
+            }:
                 msg = (
-                    "Configured model provider policy service could not be loaded; "
-                    "refusing to start with the OSS allow-all fallback"
+                    f"Configured {service_type.value} could not be loaded; "
+                    "refusing to start with the OSS allow-all fallback"
                 )
                 raise RuntimeError(msg)
             return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lfx/src/lfx/services/manager.py` around lines 536 - 541, Update the
policy bundle service loading path around load_object_from_import_path so a None
result raises a RuntimeError instead of silently falling back to the built-in
allow-all service. Match the existing strict failure behavior used for
MODEL_PROVIDER_POLICY_SERVICE, while preserving the BasePolicyBundleService
subclass validation in the POLICY_BUNDLE_SERVICE branch.
src/backend/tests/unit/services/database/test_migration_downgrade.py (2)

16-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The fixture uses a URL shape that __init__ never produces.

_service bypasses __init__ with object.__new__, then sets database_url to the sync form sqlite:////configured/production.db. A real DatabaseService rewrites this value through _sanitize_database_url, so the attribute holds sqlite+aiosqlite:////configured/production.db at runtime. The assertion at Line 38 therefore locks in a URL shape that production never reaches.

The stub on _current_alembic_revisions compounds this: it removes the only code in the downgrade path that calls sa.create_engine with that URL. Use the post-sanitization URL in the fixture, and add one test that exercises _current_alembic_revisions against a real in-memory SQLite database.

As per coding guidelines: "Warn when backend pytest files rely on excessive mocks that obscure what is actually being tested."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/database/test_migration_downgrade.py` around
lines 16 - 22, The _service fixture does not represent the post-sanitization
DatabaseService state and mocks away engine creation. Set database_url to the
aiosqlite-sanitized form, remove the _current_alembic_revisions stub where
appropriate, and add a test that exercises _current_alembic_revisions against a
real in-memory SQLite database so the downgrade path validates actual engine
usage.

Source: Coding guidelines


41-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cover the multi-head and empty-revision guard cases.

The guard uses set equality, so it also refuses a database with multiple Alembic heads and a database with no alembic_version rows. The second case produces the distinct found none message. Parametrize current_revisions over {"later_revision"}, {CURRENT_REVISION, "other_head"}, and set() to lock in all three refusals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/database/test_migration_downgrade.py` around
lines 41 - 52, The test
test_explicit_downgrade_refuses_an_unexpected_database_revision should be
parametrized over current_revisions values {"later_revision"},
{CURRENT_REVISION, "other_head"}, and set(). Update the expected RuntimeError
match so the empty set asserts the distinct “found none” message, while the
single- and multi-head cases assert their corresponding revision details; keep
downgrade.assert_not_called() for every case.
src/backend/base/langflow/services/catalog_policy/service.py (2)

70-79: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider caching the derived catalog snapshot per bundle revision.

snapshot builds a new CatalogPolicySnapshot on every access. The base class calls self.snapshot inside is_component_blocked and is_template_blocked, so a loop over many component keys constructs one object per key. The underlying frozensets are shared by reference, so a memoized projection keyed on the current bundle snapshot identity removes the repeated construction without changing behavior.

♻️ Proposed memoization of the projected snapshot
     def __init__(
         self,
         database_service: DatabaseService | None,
         policy_bundle_service: BasePolicyBundleService | None = None,
     ) -> None:
         super().__init__()
         self.database_service = database_service
         self._policy_bundle_service = policy_bundle_service
         self._legacy_snapshot = CatalogPolicySnapshot()
         self._legacy_hydrated = False
+        self._projected_source: PolicyBundleSnapshot | None = None
+        self._projected_snapshot = CatalogPolicySnapshot()
         self._write_lock = asyncio.Lock()
@@
     def snapshot(self) -> CatalogPolicySnapshot:
         """Return the current immutable process-local snapshot."""
         if self._policy_bundle_service is None:
             return self._legacy_snapshot
         bundle = self._policy_bundle_service.snapshot
-        return CatalogPolicySnapshot(
-            blocked_component_keys=bundle.blocked_component_keys,
-            blocked_template_keys=bundle.blocked_template_keys,
-        )
+        # One atomic read of the published reference, then an identity check.
+        if self._projected_source is not bundle:
+            self._projected_snapshot = CatalogPolicySnapshot(
+                blocked_component_keys=bundle.blocked_component_keys,
+                blocked_template_keys=bundle.blocked_template_keys,
+            )
+            self._projected_source = bundle
+        return self._projected_snapshot
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/catalog_policy/service.py` around lines 70
- 79, Memoize the derived CatalogPolicySnapshot in the snapshot property using
the current _policy_bundle_service.snapshot identity or revision as the cache
key. Reuse the cached projection while the bundle is unchanged, and rebuild and
replace it when the bundle changes; preserve the _legacy_snapshot path when no
policy bundle service exists.

168-196: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider a bounded retry when the facet write loses the compare-and-swap.

This branch reads the active revision and then replaces it in the same transaction. A concurrent component edit and template edit therefore make one caller receive a 409, even though the two facets do not overlap. The endpoint does not retry, so an operator must resubmit. A bounded retry loop that re-reads the current revision and re-applies only the requested facet would keep the atomic full-bundle write while removing the avoidable conflict.

Keep the current behavior if you want every conflict to be operator-visible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/catalog_policy/service.py` around lines
168 - 196, Add a bounded retry around the read-and-replace flow in the catalog
policy update method, re-reading the latest bundle state after a
compare-and-swap conflict and reapplying only the requested component or
template facet while preserving the other facet. Keep the atomic
replace_policy_bundle_state write and return the committed snapshot/diff on
success; propagate the conflict after the retry limit is exhausted.
src/backend/tests/unit/services/test_model_provider_policy_store.py (1)

156-158: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the catalog facet that this test names.

The test name states that an external provider does not block the database-owned catalog refresh. The snapshot sets blocked_component_keys={"PythonREPL"}, but no assertion checks that catalog_service observes it. Add the catalog assertion so the test covers the named behavior.

💚 Proposed assertion
     assert policy_store.apply_model_provider_policy_state(state) is True
     assert bundle_service.snapshot is snapshot
+    assert catalog_service.snapshot.blocked_component_keys == frozenset({"PythonREPL"})
     assert provider_service.approved_provider_ids == frozenset({"openai"})
As per coding guidelines: "verify the tests actually cover the new or changed behavior rather than acting as placeholders".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/services/test_model_provider_policy_store.py` around
lines 156 - 158, Update the test around apply_model_provider_policy_state to
assert that catalog_service observes the snapshot’s blocked component key,
including "PythonREPL". Keep the existing bundle and provider assertions
unchanged so the test verifies the catalog facet named by the test behavior.

Source: Coding guidelines

src/backend/base/langflow/api/v1/policy_bundle.py (1)

52-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bound the catalog key lists like the provider list.

approved_provider_ids limits the list to 1000 entries and constrains each ID with a pattern and max_length=255. blocked_component_keys and blocked_template_keys accept an unbounded number of entries with unbounded length. _normalize_keys only rejects empty strings. An oversized payload is then persisted as JSON and hashed.

Add a list bound and a per-key length bound for parity.

♻️ Proposed bounds
+CatalogKey = Annotated[str, StringConstraints(min_length=1, max_length=255)]
+
+
 class PolicyBundleWrite(BaseModel):
     """Complete replacement guarded by the caller's observed revision."""
 
     expected_revision: int = Field(ge=1)
     approved_provider_ids: Annotated[list[ProviderId], Field(max_length=1000)]
-    blocked_component_keys: list[str]
-    blocked_template_keys: list[str]
+    blocked_component_keys: Annotated[list[CatalogKey], Field(max_length=5000)]
+    blocked_template_keys: Annotated[list[CatalogKey], Field(max_length=5000)]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/api/v1/policy_bundle.py` around lines 52 - 55,
Update the policy bundle model fields blocked_component_keys and
blocked_template_keys to enforce the same catalog constraints as
approved_provider_ids: cap each list at 1000 entries and limit each key to 255
characters. Preserve the existing key normalization while applying these bounds
through the field/type declarations.
src/backend/base/langflow/api/v1/catalog_policy.py (1)

51-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share one revision-conflict response builder. Both changed sites construct the same 409 detail dictionary with message, expected_revision, and active_revision. A third copy exists in src/backend/base/langflow/api/v1/policy_bundle.py at lines 134-142. The shared root cause is a missing common helper, so the client-facing conflict contract can drift between endpoints.

  • src/backend/base/langflow/api/v1/catalog_policy.py#L51-L59: remove _revision_conflict and import the shared builder.
  • src/backend/base/langflow/api/v1/model_provider_policy.py#L147-L155: replace the inline HTTPException construction with the same shared builder.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/api/v1/catalog_policy.py` around lines 51 - 59,
Introduce or reuse one shared revision-conflict response builder, preserving the
existing 409 detail contract with message, expected_revision, and
active_revision. In src/backend/base/langflow/api/v1/catalog_policy.py#L51-L59,
remove _revision_conflict and import the shared builder; in
src/backend/base/langflow/api/v1/model_provider_policy.py#L147-L155, replace the
inline HTTPException construction with it. Also ensure the existing builder in
src/backend/base/langflow/api/v1/policy_bundle.py#L134-L142 is the common
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/base/langflow/services/database/service.py`:
- Around line 687-694: Update DatabaseService._current_alembic_revisions to use
a sync-compatible database URL for every dialect, including converting
sqlite+aiosqlite URLs produced by _sanitize_database_url, or reuse an existing
synchronous engine instead of creating a second one. Add or update coverage in
src/backend/tests/unit/services/database/test_migration_downgrade.py at lines
16-22 to verify legacy revision reads work with the sanitized SQLite URL.

---

Outside diff comments:
In `@src/backend/tests/unit/api/v1/test_policy_bundle.py`:
- Around line 295-324: Add a conflict-path test alongside
test_rollback_endpoint_creates_and_publishes_a_new_revision by configuring
rollback_state (the rollback_policy_bundle_state mock) to raise
PolicyBundleRevisionConflictError. Post the rollback request and assert HTTP
409, then verify apply_state (apply_policy_bundle_state) was not called.

---

Nitpick comments:
In
`@src/backend/base/langflow/alembic/versions/f7a9c2d4e6b8_add_shared_policy_bundle.py`:
- Around line 325-332: Update downgrade() to handle partial table state:
synchronize legacy policy data only when both REVISION_TABLE and ACTIVE_TABLE
exist, then drop each of those tables independently when it exists. Remove the
early return that currently preserves a lone table, while retaining safe
existence checks before every drop.

In `@src/backend/base/langflow/api/v1/catalog_policy.py`:
- Around line 51-59: Introduce or reuse one shared revision-conflict response
builder, preserving the existing 409 detail contract with message,
expected_revision, and active_revision. In
src/backend/base/langflow/api/v1/catalog_policy.py#L51-L59, remove
_revision_conflict and import the shared builder; in
src/backend/base/langflow/api/v1/model_provider_policy.py#L147-L155, replace the
inline HTTPException construction with it. Also ensure the existing builder in
src/backend/base/langflow/api/v1/policy_bundle.py#L134-L142 is the common
implementation.

In `@src/backend/base/langflow/api/v1/policy_bundle.py`:
- Around line 52-55: Update the policy bundle model fields
blocked_component_keys and blocked_template_keys to enforce the same catalog
constraints as approved_provider_ids: cap each list at 1000 entries and limit
each key to 255 characters. Preserve the existing key normalization while
applying these bounds through the field/type declarations.

In `@src/backend/base/langflow/services/catalog_policy/service.py`:
- Around line 70-79: Memoize the derived CatalogPolicySnapshot in the snapshot
property using the current _policy_bundle_service.snapshot identity or revision
as the cache key. Reuse the cached projection while the bundle is unchanged, and
rebuild and replace it when the bundle changes; preserve the _legacy_snapshot
path when no policy bundle service exists.
- Around line 168-196: Add a bounded retry around the read-and-replace flow in
the catalog policy update method, re-reading the latest bundle state after a
compare-and-swap conflict and reapplying only the requested component or
template facet while preserving the other facet. Keep the atomic
replace_policy_bundle_state write and return the committed snapshot/diff on
success; propagate the conflict after the retry limit is exhausted.

In `@src/backend/tests/unit/alembic/test_shared_policy_bundle_migration.py`:
- Around line 222-244: Add two negative migration tests covering the fail-closed
guards: one should create exactly one bundle table with durable rows and assert
upgrade raises the expected partial-initialization RuntimeError; the other
should create the active bundle pointer referencing a nonexistent immutable
revision and assert _seed_active_bundle raises its missing-revision
RuntimeError. Use _create_legacy_policy_tables and
migration._create_revision_table for setup, and give each test a descriptive
name.

In `@src/backend/tests/unit/services/database/test_migration_downgrade.py`:
- Around line 16-22: The _service fixture does not represent the
post-sanitization DatabaseService state and mocks away engine creation. Set
database_url to the aiosqlite-sanitized form, remove the
_current_alembic_revisions stub where appropriate, and add a test that exercises
_current_alembic_revisions against a real in-memory SQLite database so the
downgrade path validates actual engine usage.
- Around line 41-52: The test
test_explicit_downgrade_refuses_an_unexpected_database_revision should be
parametrized over current_revisions values {"later_revision"},
{CURRENT_REVISION, "other_head"}, and set(). Update the expected RuntimeError
match so the empty set asserts the distinct “found none” message, while the
single- and multi-head cases assert their corresponding revision details; keep
downgrade.assert_not_called() for every case.

In `@src/backend/tests/unit/services/test_model_provider_policy_store.py`:
- Around line 156-158: Update the test around apply_model_provider_policy_state
to assert that catalog_service observes the snapshot’s blocked component key,
including "PythonREPL". Keep the existing bundle and provider assertions
unchanged so the test verifies the catalog facet named by the test behavior.

In `@src/lfx/src/lfx/services/manager.py`:
- Around line 536-541: Update the policy bundle service loading path around
load_object_from_import_path so a None result raises a RuntimeError instead of
silently falling back to the built-in allow-all service. Match the existing
strict failure behavior used for MODEL_PROVIDER_POLICY_SERVICE, while preserving
the BasePolicyBundleService subclass validation in the POLICY_BUNDLE_SERVICE
branch.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2037d55d-094f-4b49-b141-e788ab16fd2e

📥 Commits

Reviewing files that changed from the base of the PR and between 259369e and 25f0ff6.

📒 Files selected for processing (38)
  • src/backend/base/langflow/alembic/versions/f7a9c2d4e6b8_add_shared_policy_bundle.py
  • src/backend/base/langflow/api/router.py
  • src/backend/base/langflow/api/v1/__init__.py
  • src/backend/base/langflow/api/v1/catalog_policy.py
  • src/backend/base/langflow/api/v1/model_provider_policy.py
  • src/backend/base/langflow/api/v1/policy_bundle.py
  • src/backend/base/langflow/services/catalog_policy/factory.py
  • src/backend/base/langflow/services/catalog_policy/service.py
  • src/backend/base/langflow/services/database/models/__init__.py
  • src/backend/base/langflow/services/database/models/policy_bundle/__init__.py
  • src/backend/base/langflow/services/database/models/policy_bundle/model.py
  • src/backend/base/langflow/services/database/service.py
  • src/backend/base/langflow/services/deps.py
  • src/backend/base/langflow/services/factory.py
  • src/backend/base/langflow/services/model_provider_policy.py
  • src/backend/base/langflow/services/policy_bundle.py
  • src/backend/base/langflow/services/schema.py
  • src/backend/base/langflow/services/task/model_provider_policy_refresh.py
  • src/backend/base/langflow/services/utils.py
  • src/backend/tests/unit/alembic/test_shared_policy_bundle_migration.py
  • src/backend/tests/unit/api/v1/test_catalog_policy.py
  • src/backend/tests/unit/api/v1/test_model_provider_policy.py
  • src/backend/tests/unit/api/v1/test_policy_bundle.py
  • src/backend/tests/unit/services/database/test_migration_downgrade.py
  • src/backend/tests/unit/services/test_model_provider_policy_refresh.py
  • src/backend/tests/unit/services/test_model_provider_policy_store.py
  • src/backend/tests/unit/services/test_policy_bundle_store.py
  • src/lfx/src/lfx/services/catalog_policy/__init__.py
  • src/lfx/src/lfx/services/catalog_policy/base.py
  • src/lfx/src/lfx/services/catalog_policy/service.py
  • src/lfx/src/lfx/services/deps.py
  • src/lfx/src/lfx/services/manager.py
  • src/lfx/src/lfx/services/model_provider_policy/service.py
  • src/lfx/src/lfx/services/policy_bundle/__init__.py
  • src/lfx/src/lfx/services/policy_bundle/base.py
  • src/lfx/src/lfx/services/policy_bundle/service.py
  • src/lfx/src/lfx/services/schema.py
  • src/lfx/tests/unit/services/model_provider_policy/test_policy.py

Comment thread src/backend/base/langflow/services/database/service.py
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 49%
49.94% (71956/144060) 70.31% (10066/14316) 46.88% (1653/3526)

Unit Test Results

Tests Skipped Failures Errors Time
5539 0 💤 0 ❌ 0 🔥 20m 38s ⏱️

@erichare

erichare commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Addressed the CodeRabbit review in 8bcac79fe3.

  • Added rollback conflict coverage and standardized the structured policy-bundle 409 response across all three APIs.
  • Made migration upgrade/downgrade fail closed for partial or corrupt bundle state, including missing active pointers and missing immutable revisions.
  • Added real sqlite+aiosqlite revision-read coverage plus later-revision, multi-head, and no-revision downgrade guards.
  • Bounded catalog payloads, cached bundle-to-catalog projections, and added a three-attempt facet CAS retry that preserves the latest untouched fields.
  • Added the direct catalog refresh assertion and made an explicitly configured but unloadable policy-bundle service fail startup.

For the inline SQLite concern, the production helper already strips +aiosqlite; I kept that code unchanged and added the file-backed regression test proving the path works.

Validation: 105 focused backend tests and 36 isolated LFX tests pass; Ruff, formatting, diff checks, and the pre-commit migration validator are also clean.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@erichare
erichare merged commit f953218 into release-1.12.0 Aug 5, 2026
281 of 285 checks passed
@erichare
erichare deleted the fix/shared-policy-bundle branch August 5, 2026 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant